using System;

namespace SharpMud.Server.App.Hosting
{
	/// <summary>
	/// This class provides support for dynamically loading a server app (MudLib)
	/// and then executing it
	/// </summary>
	public class ApplicationHost : IDisposable
    {
        protected static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodInfo.GetCurrentMethod().DeclaringType);

        #region Static Members
        private static ApplicationHost          _GlobalInstance;
        private static System.Threading.Mutex   _GlobalInstanceMutex;
        private const string _GlobalInstanceMutex_Name = "SharpMud Application Host";

        //static ~ApplicationHost()
        //{
        //    if (_GlobalInstance != null && _GlobalInstanceMutex!=null)
        //    {
        //        _GlobalInstance.RetractGlobal();
        //    }
        //    else if (_GlobalInstance==null && _GlobalInstanceMutex != null)
        //    {
        //        log.InfoFormat("RELEASING {System.Threading.Mutex}[\"{0}\"]", _GlobalInstanceMutex_Name);
        //        _GlobalInstanceMutex.ReleaseMutex();
        //        _GlobalInstanceMutex.Close();
        //        _GlobalInstanceMutex = null;
        //        log.InfoFormat("{System.Threading.Mutex} RELEASED[\"{0}\"]", _GlobalInstanceMutex_Name);
        //    }
        //}
        
        static internal ApplicationHost GlobalInstance
        {
            get
            {
                return _GlobalInstance;
            }
        }
        private void GoGlobal()
        {
            DemandNoPriorGlobal();
            
            bool mcs=false;

            log.InfoFormat("CREATING Mutex[\"{0}\"]...", _GlobalInstanceMutex_Name);
            _GlobalInstanceMutex = new System.Threading.Mutex(true, _GlobalInstanceMutex_Name);
            mcs = _GlobalInstanceMutex.WaitOne(0, false);
            if(mcs==false)
                throw new HostAlreadyPresentException();
            log.InfoFormat("Mutex CREATED", _GlobalInstanceMutex_Name);

            _GlobalInstance = this;
        }
        private void RetractGlobal()
        {
            if (this == GlobalInstance)
            {
                log.InfoFormat("RELEASING Mutex[\"{0}\"]", _GlobalInstanceMutex_Name);
                _GlobalInstance = null;
                _GlobalInstanceMutex.Close();
                _GlobalInstanceMutex = null;
                log.InfoFormat("Mutex RELEASED", _GlobalInstanceMutex_Name);
            }
        }
        private void DemandNoPriorGlobal()
        {
            if (GlobalInstance != null && GlobalInstance != this)
                throw new HostAlreadyPresentException();

            bool mcs = false;
            System.Threading.Mutex m = new System.Threading.Mutex(true, _GlobalInstanceMutex_Name);
            mcs = m.WaitOne(0, false);
            if (mcs == false)
                throw new HostAlreadyPresentException();
            m.Close();
            m = null;
        }
        #endregion

        #region Instance Members

        public ApplicationHost()
        {
        }

        #region Private Variables
        /// <summary>
		/// This public variable is the server app
		/// </summary>
		internal SharpMud.Server.App.Application			_App;
		private SharpMud.Server.App.IAppExecutionHandle     _AppExecHandle;

        private bool _IsRemoting;
        private System.Runtime.Remoting.Channels.IChannel _RemotingChannel;
		
		private bool _Initialized;
        #endregion

        #region Initialization
        /// <summary>
		/// This method initializes the server app host, and loads its configuration,
		/// and prepares a ServerApp that can be executed
		/// </summary>
		public void Initialize()
		{
			if(_Initialized) return;

            DemandNoPriorGlobal();

            log.Info("Initializing...");
			if(!CurrentWindowsUserIsAuthorized)
			{
				throw new HostInitializationException(new System.Security.SecurityException("The current Windows user is not authorized to Intitialize the SharpMud Application host."));
			}

			if(_Configuration==null)
			{
				throw new HostInitializationException(new InvalidOperationException("The configuration for the application host must be loaded before the app host is initialized."));
			}

            try
            {
                _App = (SharpMud.Server.App.Application)_Configuration.ServerAppLocation.AcquireTypeInstance();
            }
            catch (SharpMud.DynamicTyping.AssemblyNotFoundException e)
            {//Happens when the target assembly does not exist
                throw new HostInitializationException(e);
            }
            catch (SharpMud.DynamicTyping.TypeNotFoundException e)
            {   //Happens when the target type does not exist within the target assembly
                throw new HostInitializationException(e);
            }
            catch (InvalidCastException e)
            {   //Happens when the target type does not derive from SharpMud.Server.App.Application
                throw new HostInitializationException(e);
            }
            
			_AppExecHandle = this.CreateAppExecHandle();

            if (_Configuration.RemotingPoint != null && _Configuration.RemotingPoint != String.Empty)
            {
                _StartRemoting();
            }

            if (this._Configuration.CatchUnhandledExceptions == true)
            {
                AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
            }

            AppDomain.CurrentDomain.ProcessExit += new EventHandler(CurrentDomain_ProcessExit);

            DemandNoPriorGlobal();
            GoGlobal();

            _Initialized = true;
            log.Info("INITALIZED");
		}

        void CurrentDomain_ProcessExit(object sender, EventArgs e)
        {
            log.Info("[CurrentDomain_ProcessExit]!");
            if (_App != null)
            {
                if (!(_App.Status == AppStatus.Stopping || _App.Status == AppStatus.Stopped || _App.Status == AppStatus.Disposed))
                {
                    _App.___Stop();
                }
                if (_App.Status == AppStatus.Stopping)
                    WaitForServerStop();
                _App.Dispose();
                _App = null;
            }
        }

        private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            log.Fatal("[CurrentDomain_UnhandledException]! CLR-Terminating(" + e.IsTerminating.ToString() + ")", (System.Exception)e.ExceptionObject);
            OnUnhandledException(sender, e);
        }

        protected virtual void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            if (e.IsTerminating)
            {
                if(e is System.Threading.ThreadAbortException)
                    System.Threading.Thread.ResetAbort();
                string s = "CLR-Terminating exception has occured";
                if (_App != null)
                {
                    if (!(_App.Status == AppStatus.Stopping || _App.Status == AppStatus.Stopped || _App.Status == AppStatus.Disposed))
                    {
                        s += "The server will be stopped and disposed.";
                    }
                    else if (_App.Status == AppStatus.Stopping)
                    {
                        s += "The server was already stopping at the time. It will be disposed.";
                    }
                }
                log.Fatal(s);
                if (!(_App.Status == AppStatus.Stopping || _App.Status == AppStatus.Stopped || _App.Status == AppStatus.Disposed))
                {
                    _App.___Stop();
                }
                if (_App.Status == AppStatus.Stopping)
                    WaitForServerStop();
                _App.Dispose();
                _App = null;
            }
            else
            {
            }
        }

        private void _StartRemoting()
        {
            if (_IsRemoting)
                throw new InvalidOperationException();
            System.Uri rURI = _Configuration.RemotingPointUri;
            switch(rURI.Scheme.ToLower().Trim())
            {
                case("http"):
                    _RemotingChannel = new System.Runtime.Remoting.Channels.Http.HttpServerChannel(rURI.Port);
                    break;
                case("tcp"):
                    _RemotingChannel = new System.Runtime.Remoting.Channels.Tcp.TcpServerChannel(rURI.Port);
                    break;
                case("ipc"):
                    _RemotingChannel = new System.Runtime.Remoting.Channels.Ipc.IpcServerChannel(rURI.Host);
                    break;
                default:
                    throw new System.InvalidOperationException("Unknown remoting protocol: " + _Configuration.RemotingPointUri.Scheme );
                    break;
            }
            try
            {
                System.Runtime.Remoting.Channels.ChannelServices.RegisterChannel(_RemotingChannel, false);
            }
            catch(System.Exception e)
            {
                log.Error("There was an error registering the remoting channel: " + System.Environment.NewLine
                + "\tChannel Type: " + _RemotingChannel.GetType().ToString(), e);
            }
            try
            {
                System.Runtime.Remoting.RemotingConfiguration.RegisterWellKnownServiceType(typeof(AppExecutionHandle), rURI.PathAndQuery, System.Runtime.Remoting.WellKnownObjectMode.SingleCall);
            }
            catch (System.Exception e)
            {
                log.Error("There was an error registering the IAppExecHandle with the remoting server: " + System.Environment.NewLine
                + "\tURI:   " + rURI.ToString(), e);
            }

            log.Info("'IAppExecutionHandle' remoting at \"" + rURI.ToString() + "\" ESTABLISHED");

            _IsRemoting = true;
        }

        private void _StopRemoting()
        {
            if (!_IsRemoting)
                throw new InvalidOperationException();

            System.Runtime.Remoting.Channels.ChannelServices.UnregisterChannel(_RemotingChannel);

            log.Info("'IAppExecutionHandle' remoting at \"" + this._Configuration.RemotingPointUri.ToString() + "\" TERMINATED" );

            _IsRemoting = false;
        }

        #region IDisposable Members

        public void Dispose()
        {
            log.Info("DISPOSING");

            _Dispose();
            if(App.Status == AppStatus.Running)
                AppExecHandle.Stop();
            AppExecHandle.Dispose();
            if(this._IsRemoting) this._StopRemoting();
            _AppExecHandle = null;
            _App = null;

            System.Diagnostics.Debug.Assert(_GlobalInstance == this);
            this.RetractGlobal();

            log.Info("DISPOSED");
        }
        protected virtual void _Dispose()
        {
        }

        #endregion
#endregion

        #region Security Related

        /// <summary>
		/// Determins if the current windows user is authorized to manipulate the server app.
		/// Returns true is the current Windows user is System or is an Administrator,
		/// returns false if the current Windows user is Anonymous or is a Guest, and otherwise
		/// returns true.
		/// </summary>
		public bool CurrentWindowsUserIsAuthorized
		{
			get
			{
				System.Security.Principal.WindowsIdentity currentWindowsIdent = System.Security.Principal.WindowsIdentity.GetCurrent();
				System.Security.Principal.WindowsPrincipal currentWindowsPrincipal = new System.Security.Principal.WindowsPrincipal(currentWindowsIdent);
				if(currentWindowsIdent.IsSystem==true)
					return true;
				if(currentWindowsIdent.IsAnonymous==true)
					return false;
				if(currentWindowsPrincipal.IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator))
					return true;
				if(currentWindowsPrincipal.IsInRole(System.Security.Principal.WindowsBuiltInRole.Guest))
					return false;
				return true;
			}
		}
		/// <summary>
		/// Gets a new application execution handle for the server application
		/// </summary>
		/// <exception cref="System.Security.SecurityException">Thrown if the current Windows user is not authorized to manipulate the server app</exception>
		public SharpMud.Server.App.IAppExecutionHandle CreateAppExecHandle()
		{
			if(!CurrentWindowsUserIsAuthorized)
				throw new System.Security.SecurityException("The current Windows user is not authorized to acquire/create an Application Execution Handle for a SharpMud Application.");
			return new AppExecutionHandle(this._App);
		}
		/// <summary>
		/// Gets the application execution handle, the one that the app host is using, for the server application
		/// </summary>
		/// <exception cref="System.Security.SecurityException">Thrown if the current Windows user is not authorized to manipulate the server app</exception>
		public SharpMud.Server.App.IAppExecutionHandle GetAppExecHandle()
		{
			if(!CurrentWindowsUserIsAuthorized)
				throw new System.Security.SecurityException("The current Windows user is not authorized to acquire/create an Application Execution Handle for a SharpMud Application.");
			return this._AppExecHandle;
        }
        /// <summary>
        /// Gets the server application execution handle that the server app host is using
        /// </summary>
        /// <exception cref="System.Security.SecurityException">Thrown if the current Windows user is not authorized to manipulate the server app</exception>
        public SharpMud.Server.App.IAppExecutionHandle AppExecHandle
        {
            get
            {
                if (!CurrentWindowsUserIsAuthorized)
                    throw new System.Security.SecurityException("The current Windows user is not authorized to acquire/create an Application Execution Handle for a SharpMud Application.");
                return this._AppExecHandle;
            }
        }
        #endregion

        #region Direct App Access
        /// <summary>
		/// Gets the server application object
		/// </summary>
		/// <exception cref="System.Security.SecurityException">Thrown if the current Windows user is not authorized to manipulate the server app</exception>
		public SharpMud.Server.App.Application App
		{
			get
			{
				if(!CurrentWindowsUserIsAuthorized)
					throw new System.Security.SecurityException("The current Windows user is not authorized to access the SharpMud Application object.");
				return this._App;
			}
		}

		/// <summary>
		/// Gets the status of the server application.
		/// </summary>
		/// <remarks>Uses the app host's application execution handle to acquire the status, but,
		/// doesn't require the asking user to be authorized to manipulate the server -
		/// because we aren't manipulating it.</remarks>
		public SharpMud.Server.App.AppStatus		AppStatus
		{
			get
			{
				return this._AppExecHandle.Status;
			}
        }
        #endregion
        
        #region Utility
        /// <summary>
		/// Pauses the current thread until the server is done executing
		/// </summary>
		public void WaitForServerStop()
		{
            System.TimeSpan waitInterval = System.TimeSpan.FromSeconds(0.3);
		    log.InfoFormat("waiting for Applicaton state [AppStatus.STOPPED][{0}ms]",waitInterval.TotalMilliseconds);
            while(this._AppExecHandle.Status!=AppStatus.Stopped)
            {
                try
			    {
                    System.Threading.Thread.Sleep(waitInterval);
			    }
			    catch(System.Threading.ThreadAbortException e)
			    {
                    log.Warn("ThreadAbortException caught while sleeping");
				    System.Threading.Thread.ResetAbort();
                    log.Warn("System.Threading.Thread.ResetAbort() called, server STOP signal sent");
				    this._AppExecHandle.Stop();
			    }
            }
            log.Info("Done waiting for Application STOP");
            
            this.AppExecHandle.Dispose();
		}
        #endregion

        #region Configuration
        /// <summary>
		/// The default name of a file that contains configuration information for a ApplicationHost
		/// </summary>
		public const string DefaultConfigurationFile = @"SharpMud.Server.App.config";
		protected ApplicationHostConfiguration _Configuration;
		
		/// <summary>
		/// This method loads the configuration of the ApplicationHost from the default config file
		/// </summary>
		public void LoadConfiguration()
		{
			this.LoadConfiguration(
				Application.GetSubfileIntelligently(this.RootConfigFolder,DefaultConfigurationFile)
				);
		}

		/// <summary>
		/// This method loads the configuration of the ApplicationHost from the given file
		/// </summary>
		public void LoadConfiguration(string configurationFile)
		{
			if(this._Configuration!=null) return;

			System.IO.FileStream fs;
			try
			{
				fs = new System.IO.FileStream(configurationFile,System.IO.FileMode.Open);
			}
			catch(System.IO.FileNotFoundException e)
			{
				throw new Exception("FileNotFound: '" + configurationFile + "'",e);
			}
			catch(System.IO.DirectoryNotFoundException e)
			{
				throw new Exception("DirectoryNotFound: '" + System.IO.Path.GetDirectoryName(configurationFile) + "'",e);
			}
				
			System.Xml.Serialization.XmlSerializer xs;
			try
			{
				xs = GetConfigSerializer();
			}
			catch(System.Exception e)
			{
                log.Fatal("Error while creating Application Host Configuration serializer", e);
				throw;
				return;
			}
			try
			{
				this._Configuration = (ApplicationHostConfiguration)xs.Deserialize(fs);
			}
			catch(System.Exception e)
			{
                log.Fatal("Error deserializing Application Host Configuration", e);
                throw;
				return;
			}
			fs.Close();
		}
		/// <summary>
		/// This method loads the configuration of the ApplicationHost from the configuration object
		/// </summary>
		public void LoadConfiguration(SharpMud.Server.App.Hosting.ApplicationHostConfiguration appHostConfig)
		{
			if(this._Configuration!=null) return;

			if(appHostConfig==null)
				throw new ArgumentNullException("appHostConfig");

			this._Configuration = appHostConfig;
		}

		/// <summary>
		/// This method returns a customly configured XmlSerializer for saving/loading
		/// a ApplicationHost's configuration file
		/// </summary>
		/// <returns></returns>
		public System.Xml.Serialization.XmlSerializer GetConfigSerializer()
		{
			System.Xml.Serialization.XmlSerializer xs;
			//try
			{
				xs = new System.Xml.Serialization.XmlSerializer(
					typeof(ApplicationHostConfiguration)
					//				,
					//				new System.Type[] {
					//									  typeof(DynamicTyping.TypeLocation) //typeof(DynamicTyping.AssembliedTypeLocation) 
					//								  }
					);
			}
			return xs;
		}
		/// <summary>
		/// This method saves the configuration of the ApplicationHost to the default location
		/// </summary>
		public void SaveConfiguration()
		{
			SaveConfiguration(
				Application.GetSubfileIntelligently(this.RootConfigFolder,DefaultConfigurationFile)
				);
		}
		/// <summary>
		/// This method saves the configuration of the ApplicationHost to the given file
		/// </summary>
		/// <param name="configFileName">The file to save the ApplicationHost's configuration to</param>
		public void SaveConfiguration(string configFileName)
		{
			System.Xml.Serialization.XmlSerializer xs = GetConfigSerializer();
			System.IO.FileStream fs;
			//try
			{
				fs = new System.IO.FileStream(configFileName,System.IO.FileMode.Create);
			}
			xs.Serialize(fs,_Configuration);
			fs.Close();
		}
		#endregion

		#region File System Tie-Ins
		/// <summary>
		/// This property gets/sets the root folder where configuration information
		/// is kept for a SharpMud server
		/// </summary>
		public virtual string RootConfigFolder
		{
			get
			{
				return Application.GetSubfolderIntelligently(RootDataFolder,"config","configuration");
			}
			set
			{
				throw new NotSupportedException("The RootConfigFolder cannot be changed when using the default implementation of that property.");
			}
		}
		/// <summary>
		/// This property gets/sets the root folder where DLLs and EXEs are kept
		/// for a SharpMud server
		/// </summary>
		public virtual string RootBinFolder
		{
			get
			{
                return (typeof(System.Threading.IRequiresExternalPulsing)).Assembly.Location;
                //return Application.GetSubfolderIntelligently(RootFileFolder,"bin","binaries");
			}
			set
			{
				throw new NotSupportedException("The RootBinFolder cannot be changed when using the default implementation of that property.");
			}
		}
		/// <summary>
		/// This property gets/sets the root folder where data is kept for a SharpMud server instance.
        /// Unless overidden by a derived class, this property is read-only.
		/// </summary>
		public virtual string RootDataFolder
		{
			get
			{
				return Application.GetSubfolderIntelligently(RootFileFolder,"data");
			}
			set
			{
				throw new NotSupportedException("The RootDataFolder cannot be changed when using the default implementation of that property.");
			}
		}
		/// <summary>
		/// This property gets the root folder where all files for a SharpMud MUD 
        /// server instance are kept
		/// </summary>
		private string RootFileFolder
		{
			get
			{	
				//If the current directory is "Bin" or "Binaries", then the Root File Folder
				//ist the next folder up, otherwise, use the current folder

				string CD = System.IO.Directory.GetCurrentDirectory();
				string CDn = (new System.IO.DirectoryInfo(CD)).Name;
				if(CDn.ToLower()=="bin" || CDn.ToLower()=="binaries")
				{
					return (new System.IO.DirectoryInfo(CD)).Parent.FullName;
				}
				else
				{
					return CD;
				}
			}
		}

        public string ExecutableFolder
        {
            get
            {
                return System.Reflection.Assembly.GetEntryAssembly().Location;
            }
        }

		#endregion
        #endregion
    }
}
